package com.mapforge.gui;

import java.awt.Component;
import java.awt.Container;
import java.awt.Point;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Stack;

import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.SwingUtilities;
import javax.swing.TransferHandler;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.text.JTextComponent;

/**
 * This class allows display and management via drag-and-drop of separate
 * panels. Mainly, it is used to display and manage the various layers in a
 * tiled map.
 * 
 * @author Will Morrison
 * 
 */
public class TransferableContainer extends Box {

	private static final long serialVersionUID = 1L;

	private List<TransferablePanel> allPanels;
	private List<TransferablePanel> selectedPanels;
	private transient List<ListSelectionListener> listListeners;
	TransferablePanel lastSelectedPanel;

	private PanelDropTargetListener dropTargetListener;
	private MouseListener mouseSelectionListener;
	private MouseAdapter drag_drop_mouseListener;

	private DropTarget dropTarget;

	private Comparator<Component> comparator;

	private static final DataFlavor TRANSFERABLE_CONTAINER_FLAV = new DataFlavor(
			TransferablePanel.class, "Transferable Container");

	public TransferableContainer() {
		super(BoxLayout.Y_AXIS);
		comparator = new YComparator();
		allPanels = new ArrayList<TransferablePanel>();
		selectedPanels = new ArrayList<TransferablePanel>();
		listListeners = new ArrayList<ListSelectionListener>();

		mouseSelectionListener = new PanelSelectionMouseAdapter();
		drag_drop_mouseListener = new DragDropMouseAdapter();
		dropTargetListener = new PanelDropTargetListener();
		dropTarget = new DropTarget(this, dropTargetListener);
		this.setDropTarget(dropTarget);
		addMouseListener(mouseSelectionListener);
	}

	public void addListSelectionListener(ListSelectionListener listener) {
		listListeners.add(listener);
	}

	public void clearListSelectionListeners() {
		listListeners.clear();
	}

	public boolean removeListSelectionListener(ListSelectionListener listener) {
		return listListeners.remove(listener);
	}

	protected void addImpl(Component component, Object constraints, int index) {
		if (component instanceof TransferablePanel) {
			super.addImpl(component, constraints, index);
		} else {
			throw new IllegalArgumentException(
					"Can only add Transferable Panels to a transferable container");
		}
	}

	public static DataFlavor getDataFlavor() {
		return TRANSFERABLE_CONTAINER_FLAV;
	}

	public void clearTransferablePanels() {
		for (int i = 0; i < allPanels.size(); i++) {
			this.remove(allPanels.get(i));
		}
		allPanels.clear();
		selectedPanels.clear();
		lastSelectedPanel = null;
		ListSelectionEvent event = new ListSelectionEvent(this, -1, -1, false);
		this.fireListSelectionEvent(event);
		validate();
		repaint();
	}

	public void addTransferablePanel(TransferablePanel comp) {
		DropTarget dropTarget = new DropTarget(comp, dropTargetListener);
		comp.setDropTarget(dropTarget);
		comp.addMouseListener(mouseSelectionListener);
		comp.addMouseMotionListener(drag_drop_mouseListener);
		Component[] children = ((Container) comp).getComponents();
		Stack<Component> stack = new Stack<Component>();
		Component c;
		for (int i = 0; i < children.length; i++) {
			stack.add(children[i]);
		}
		while (!stack.isEmpty()) {
			c = stack.pop();
			c.setDropTarget(dropTarget);
			c.addMouseListener(mouseSelectionListener);
			c.addMouseMotionListener(drag_drop_mouseListener);
			if (c instanceof Container) {
				Container con = (Container) c;
				Component[] comps = con.getComponents();
				for (int i = 0; i < comps.length; i++) {
					stack.add(comps[i]);
				}
			}
		}
		allPanels.add(comp);
		super.add(comp);
	}

	public int getNumberSelectedPanels() {
		return selectedPanels.size();
	}

	public TransferablePanel getTransferablePanel(int index) {
		if (allPanels.size() <= index) {
			throw new IndexOutOfBoundsException();
		} else
			return allPanels.get(index);
	}

	public void removeTransferablePanel(TransferablePanel panel) {
		super.remove(panel);
		this.allPanels.remove(panel);
		this.selectedPanels.remove(panel);
		if (lastSelectedPanel == panel)
			lastSelectedPanel = null;
	}

	private class DragDropMouseAdapter extends MouseAdapter {
		// TODO remove this junk
		// public void mouseClicked(MouseEvent e) {
		// Object src = e.getSource();
		// if (!(src instanceof JTextComponent)) {
		// Component c = (Component) src;
		// while (!(c instanceof TransferablePanel)) {
		// c = c.getParent();
		// }
		// TransferablePanel p = (TransferablePanel) c;
		// p.requestFocus();
		// if (!p.isSelected()) {
		// mouseSelectionListener.mouseClicked(e);
		// }
		// }
		// }

		public void mouseDragged(MouseEvent e) {
			Object src = e.getSource();
			if (src instanceof Component) {
				Component c = (Component) src;
				while (!(c instanceof TransferablePanel)) {
					c = c.getParent();
				}
				TransferablePanel p = (TransferablePanel) c;
				p.requestFocus();
				if (!p.isSelected()) {
					// select this panel if its being dragged
					mouseSelectionListener.mouseClicked(e);
				}
			}
			exportPanel(e);
		}

		private void exportPanel(MouseEvent e) {
			Object src = e.getSource();
			if (src instanceof Component) {
				Component c = (Component) src;
				while (!(c instanceof TransferablePanel)) {
					c = c.getParent();
				}
				if (c == null) {
					throw new IllegalArgumentException(
							"TransferableContainer can hold only transferable panels");
				}
				TransferablePanel transferablePanel = (TransferablePanel) c;
				TransferHandler handler = transferablePanel
						.getTransferHandler();
				handler
						.exportAsDrag(transferablePanel, e,
								TransferHandler.COPY);
			}
		}
	}

	public Iterator<TransferablePanel> getSelectedPanels() {
		return selectedPanels.iterator();
	}

	public Iterator<TransferablePanel> getAllPanels() {
		return allPanels.iterator();
	}

	protected final void fireListSelectionEvent(ListSelectionEvent event) {
		for (int i = 0; i < listListeners.size(); i++) {
			listListeners.get(i).valueChanged(event);
		}
	}

	protected void beginDragAndDrop() {
	}

	protected void endDragAndDrop() {

	}

	private class PanelSelectionMouseAdapter extends MouseAdapter {
		public void mouseClicked(MouseEvent e) {
			Point point = e.getPoint();
			point = SwingUtilities.convertPoint((Component) e.getSource(),
					point, TransferableContainer.this);
			Component c = getComponentAt(point);
			if (c instanceof TransferablePanel) {
				TransferablePanel transferablePanel = (TransferablePanel) c;
				// give panel focus if component clicked on wasn't a textfield
				if (!(e.getSource() instanceof JTextComponent)) {
					transferablePanel.requestFocus();
				}
				// if control isn't down then we'll make a whole new selection
				if (!e.isControlDown() || e.isShiftDown()) {
					unselectPanels();
				}
				// panels between the previously selected panel and this panel
				// should be selected.
				if (e.isShiftDown() && lastSelectedPanel != null) {
					int smallY = transferablePanel.getY();
					int bigY = lastSelectedPanel.getY();
					if (smallY > bigY) {
						int temp = bigY;
						bigY = smallY;
						smallY = temp;
					}
					smallY = Math.min(smallY, bigY);
					bigY = Math.max(smallY, bigY);
					int y;
					TransferablePanel tempPanel;
					for (int i = 0; i < allPanels.size(); i++) {
						tempPanel = allPanels.get(i);
						y = tempPanel.getY();
						if (!selectedPanels.contains(tempPanel) && y >= smallY
								&& y <= bigY) {
							tempPanel.setSelected(true);
							selectedPanels.add(tempPanel);
						}
					}
				}
				// select chosen panel
				if (!transferablePanel.isSelected()) {
					transferablePanel.setSelected(true);
					if (!selectedPanels.contains(transferablePanel)) {
						selectedPanels.add(transferablePanel);
					}
				}
				if (!e.isShiftDown()) {
					lastSelectedPanel = transferablePanel;
				}
			} else if (c instanceof Container) {
				unselectPanels();
				lastSelectedPanel = null;
			}
			// inform list listeners selection has changed
			ListSelectionEvent event = new ListSelectionEvent(
					TransferableContainer.this, 0, 0, false);
			fireListSelectionEvent(event);
		}

		private void unselectPanels() {
			Iterator<TransferablePanel> it = selectedPanels.iterator();
			while (it.hasNext()) {
				it.next().setSelected(false);
			}
			selectedPanels.clear();
		}
	}

	@Override
	public void remove(Component c) {
		super.remove(c);
		allPanels.remove(c);
	}

	/**
	 * The drop target listener for the layer scroll pane.
	 * 
	 * @author Will Morrison
	 * 
	 */
	private class PanelDropTargetListener implements DropTargetListener {
		public void dragEnter(DropTargetDragEvent e) {
		}

		public void dragExit(DropTargetEvent e) {
		}

		public void dragOver(DropTargetDragEvent e) {
		}

		public void drop(DropTargetDropEvent dtde) {
			// Just going to grab the expected DataFlavor to make sure
			// we know what is being dropped
			DataFlavor dragAndDropPanelFlavor = null;
			Object transferableObj = null;
			Transferable transferable = null;
			try {
				// Grab expected flavor
				dragAndDropPanelFlavor = TransferableContainer.getDataFlavor();
				transferable = dtde.getTransferable();
				// What does the Transferable support?
				if (transferable.isDataFlavorSupported(dragAndDropPanelFlavor)) {
					transferableObj = transferable
							.getTransferData(dragAndDropPanelFlavor);
				} else {
					throw new IllegalArgumentException(
							"Did not support transfer flavor for Drag and Drop.");
				}

			} catch (Exception ex) {
				System.err
						.println("Error occurred while performing drag and drop "
								+ transferableObj
								+ " "
								+ transferable
								+ " "
								+ dtde + " " + dragAndDropPanelFlavor);
				ex.printStackTrace();
			}
			// If didn't find an item, bail
			if (transferableObj == null) {
				return;
			}
			TransferablePanel droppedPanel = (TransferablePanel) transferableObj;
			if (droppedPanel == dtde.getTransferable()) {
				return;
			}
			// Get the y offset from the top of the WorkFlowSheetPanel
			// for the drop option (the cursor on the drop)

			Component parent = dtde.getDropTargetContext().getComponent();
			if (parent != TransferableContainer.this) {
				while (!allPanels.contains(parent)) {
					if (parent.getParent() == null)
						break;
					parent = parent.getParent();
				}
			}
			// dropped on parent, do nothing
			if (droppedPanel == parent) {
				return;
			}
			Point p = dtde.getLocation();
			// dropped on self, going nowhere
			if (p.y >= droppedPanel.getY()
					&& p.y <= droppedPanel.getY() + droppedPanel.getHeight()) {
				return;
			}
			// ignore if drop target was already selected
			Component c = parent.getComponentAt(dtde.getLocation());
			if (c != null && c instanceof TransferablePanel) {
				TransferablePanel panel = (TransferablePanel) c;
				if (panel.isSelected()) {
					return;
				}
			} else {
				return;
			}

			int dropYLoc = parent == TransferableContainer.this ? dtde
					.getLocation().y : parent.getLocation().y;
			dropYLoc++;

			beginDragAndDrop();
			Collections.sort(selectedPanels, comparator);
			allPanels.removeAll(selectedPanels);
			for (int i = 0; i < selectedPanels.size(); i++) {
				remove(selectedPanels.get(i));
			}
			validate();
			Collections.sort(allPanels, comparator);
			removeAll();
			// add all the panels in sorted order
			boolean trigger = false;
			for (int i = 0; i < allPanels.size(); i++) {
				TransferablePanel panel = allPanels.get(i);
				add(panel);
				// encountered highest selected panel, add all selected panels
				// behind it
				if (!trigger && c == panel) {
					trigger = true;
					for (int j = 0; j < selectedPanels.size(); j++) {
						add(selectedPanels.get(j));
					}
				}
			}
			allPanels.addAll(selectedPanels);
			validate();
			// sort all panels again, since the GUI layout might not necessarily
			// match the data structure
			Collections.sort(allPanels, comparator);
			endDragAndDrop();
		}

		public void dropActionChanged(DropTargetDragEvent e) {

		}

	}

	// public static void main(String[] args) {
	// TransferableContainer p = new TransferableContainer();
	// JFrame f = new JFrame();
	// Dimension d = new Dimension(200, 100);
	// for (int i = 0; i < 5; i++) {
	// TransferablePanel p2 = new TransferablePanel();
	// // JPanel p2 = new JPanel(new BorderLayout());
	// p2.setPreferredSize(d);
	// p2.setMaximumSize(d);
	// p2.setOpaque(true);
	// p2.setName("Panel " + i);
	// p2.add(new TextFieldLabel("WEST"), BorderLayout.WEST);
	// p2.add(new JButton("SOUTH " + i), BorderLayout.SOUTH);
	// p2.add(new JCheckBox("EAST"), BorderLayout.EAST);
	// p2.add(new JLabel("NORTH: " + i), BorderLayout.NORTH);
	// p2.add(new JLabel("CENTER"), BorderLayout.CENTER); //
	// f.add(p2,BorderLayout.CENTER);
	// p.addTransferablePanel(p2);
	// }
	// f.add(new JScrollPane(p));
	// f.pack();
	// f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
	// f.setLocationRelativeTo(null);
	// f.setVisible(true);
	// }

	private static class YComparator implements Comparator<Component> {
		public int compare(Component c1, Component c2) {
			int y1 = SwingUtilities.convertPoint(c1, 0, c1.getY(), c1
					.getParent()).y;
			int y2 = SwingUtilities.convertPoint(c2, 0, c2.getY(), c2
					.getParent()).y;
			if (y1 < y2)
				return -1;
			else if (y1 > y2)
				return 1;
			else
				return 0;
		}
	}
}